/**********************************************************************

  Audacity: A Digital Audio Editor

  @file Observer.h
  @brief Publisher-subscriber pattern, also known as Observer

  Paul Licameli

**********************************************************************/
#ifndef __AUDACITY_OBSERVER__
#define __AUDACITY_OBSERVER__

#include <cassert>
#include <functional>
#include <memory>
#include <type_traits>

namespace Observer {
// See below
template<typename Message, bool NotifyAll>
class Publisher;

//! Default message type for Publisher
struct Message {};

//! May be supplied to constructor of Publisher to customize exception handling
struct UTILITY_API ExceptionPolicy {
    virtual ~ExceptionPolicy() noexcept;
    //! Called at the start of each publication
    virtual void OnBeginPublish() = 0;
    // In a catch block, can rethrow or store std::current_exception()
    /*! @return if true, then skip remaining callbacks */
    virtual bool OnEachFailedCallback() noexcept(false) = 0;
    //! Called at the end of each publication, if exiting normally; may throw
    virtual void OnEndPublish() noexcept(false) = 0;
};

class Subscription;

//! Type-erased implementation helpers for Publisher
namespace detail {
struct RecordBase;
struct RecordLink {
    std::shared_ptr<RecordBase> next;
};
//! doubly-linked list cell using shared and weak pointers
struct UTILITY_API RecordBase : RecordLink {
    std::weak_ptr<RecordLink> prev;
    void Unlink() noexcept;
};
struct UTILITY_API RecordList : RecordLink, std::enable_shared_from_this<RecordLink> {
    //! Type of function visiting the list; stop visit on true return
    using Visitor = bool (*)(const RecordBase& record, const void* arg);
    //! @pre `visitor != nullptr`
    explicit RecordList(ExceptionPolicy* pPolicy, Visitor visitor);
    ~RecordList() noexcept;
    //! @pre `pRecord != nullptr`
    Subscription Subscribe(std::shared_ptr<RecordBase> pRecord);
    bool Visit(const void* arg);
private:
    ExceptionPolicy* const m_pPolicy;
    const Visitor m_visitor;
};
}

//! A move-only handle representing a connection to a Publisher
class UTILITY_API Subscription
{
public:
    Subscription();
    Subscription(Subscription&&);
    Subscription& operator=(Subscription&&);

    //! Calls Reset
    ~Subscription() noexcept { Reset(); }

    //! Breaks the connection (constant time)
    void Reset() noexcept;

    /*!
       @return true iff there is no connection
       (Publisher was destroyed, or this was not reassigned since it was last
       Reset(), default-constructed, or moved from)
     */
    bool Expired() const { return m_wRecord.expired(); }

    //! @return not expired
    explicit operator bool() const {
        return !Expired();
    }

private:
    friend detail::RecordList;
    explicit Subscription(std::weak_ptr<detail::RecordBase> pRecord);
    std::weak_ptr<detail::RecordBase> m_wRecord;
};

//! An object that sends messages to an open-ended list of subscribed callbacks
/*!
 Intended for single-threaded use only.
 Move-only; allows type-erased custom allocation.

 @tparam Message the type of message
 @tparam NotifyAll if true, callback return values are ignored; else, a callback
   returning true causes earlier subscribed callbacks to be skipped
 */
template<typename Message = Message, bool NotifyAll = true>
class Publisher
{
public:
    //! An implementation class, public so it can be your custom allocator's template parameter
    struct Record;

    static constexpr bool notifies_all = NotifyAll;
    using message_type = Message;

    //! Constructor supporting type-erased custom allocation/deletion
    /*!
     @param pPolicy if null, exceptions from callbacks are caught and ignored;
     else, *pPolicy must have a lifetime encompassing the Publisher's
     */
    template<typename Alloc = std::allocator<Record> >
    explicit Publisher(ExceptionPolicy* pPolicy = nullptr, Alloc a = {});

    Publisher(Publisher&&) = default;
    Publisher& operator=(Publisher&&) = default;

    using CallbackReturn = std::conditional_t<NotifyAll, void, bool>;

    //! Type of functions that can be connected to the Publisher
    using Callback = std::function< CallbackReturn (const Message&) >;

    //! Connect a callback to the Publisher; later-connected are called earlier
    /*!
     During Publish(), the callback may have the side-effect of adding or
     removing other Subscriptions.  Added subscriptions will not be called,
     and removed ones, if not called already, will not be called after.

     @pre `callback != nullptr`
     */
    [[nodiscard]] Subscription Subscribe(Callback callback);

    //! Overload of Subscribe takes an object and pointer-to-member-function
    template<typename Object, typename Return, typename ... Args>
    Subscription Subscribe(
        Object& obj, Return (Object::*callback)(Args...))
    {
        return Subscribe([&obj, callback](const Message& message){
            return (obj.*callback)(message);
        });
    }

    struct Record : detail::RecordBase {
        explicit Record(Callback callback)
            : callback{std::move(callback)} {}
        Callback callback;
    };

protected:
    //! Send a message to connected callbacks
    /*! Later-connected are called earlier; if !NotifyAll, any callback may
      return true, to stop the notification; see also class ExceptionPolicy

     @return if `NotifyAll`, then void; else, whether the visit was stopped
     (either because a callback returned true, or the exception policy consumed
     an exception, and ordered a stop)
     */
    CallbackReturn Publish(const Message& message);

private:
    // RecordList needs to be non-relocating but the Publisher object is movable
    std::shared_ptr<detail::RecordList> m_list;
    std::function<std::shared_ptr<detail::RecordBase>(Callback)> m_factory;
};

template<typename Message, bool NotifyAll>
template<typename Alloc> inline
Publisher<Message, NotifyAll>::Publisher(ExceptionPolicy* pPolicy, Alloc a)
    : m_list{std::allocate_shared<detail::RecordList>(a, pPolicy,
                                                      []( // The visitor.  Lambda with no capture converts to pointer-to-function
                                                          const detail::RecordBase& recordBase, const void* arg){
        auto& record = static_cast<const Record&>(recordBase);
        assert(arg);
        auto& message = *static_cast<const Message*>(arg);
        assert(record.callback);
        // Calling foreign code!  Which is why we have an exception policy.
        if constexpr (NotifyAll) {
            return record.callback(message), false;
        } else {
            return record.callback(message);
        }
    }
                                                      )},
    m_factory([a = std::move(a)](Callback callback) {
    return std::allocate_shared<Record>(a, std::move(callback));
})
{
}

// Thin wrappers of type-erased worker functions:

template<typename Message, bool NotifyAll>
auto Publisher<Message, NotifyAll>::Subscribe(Callback callback)
-> Subscription
{
    assert(callback); // precondition
    return m_list->Subscribe(m_factory(std::move(callback)));
}

template<typename Message, bool NotifyAll> inline
auto Publisher<Message, NotifyAll>::Publish(const Message& message)
-> CallbackReturn
{
    bool result = m_list->Visit(&message);
    if constexpr (!NotifyAll) {
        return result;
    }
}
}

#endif
